﻿using PerfTestLib.Result;
using PerfTestLib.Spec;
using PerfTestLib.Utility.Parallel;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace PerfTestLib.Runtime
{
    /// <summary>
    /// Test runner.
    /// </summary>
    /// <typeparam name="TData"> Type of data. </typeparam>
    public class TestRunner<TData>
    {
        public CancellationTokenSource CancellationSource { get; set; }
        private TestSpec<TData> Spec { get; set; }
        private Func<TestSpecIterationInstance<TData>, object> TestAction { get; set; }
        private ITestSpecResultRepository SpecResultRepo { get; set; }

        public TestRunnerProgress Progress { get; private set; }

        // currently running task
        private ConcurrentQueue<Task> RunningTasks { get; set; }


        /// <summary>
        /// Ctor.
        /// </summary>
        public TestRunner(TestSpec<TData> spec, Func<TestSpecIterationInstance<TData>, object> testAction, ITestSpecResultRepository specResultRepo)
        {
            this.CancellationSource = new CancellationTokenSource();
            this.Spec = spec;
            this.TestAction = testAction;
            this.SpecResultRepo = specResultRepo;
            this.Progress = new TestRunnerProgress();
            this.RunningTasks = new ConcurrentQueue<Task>();
        }


        /// <summary>
        /// Run tests.
        /// </summary>
        /// <returns> Task. </returns>
        public Task<TestSpecResult> Run()
        {
            // create spec result
            TestSpecResult specResult = new TestSpecResult();
            specResult.SpecId = this.Spec.Id;

            // set progress
            this.Progress.Reset();
            this.Progress.SpecId = this.Spec.Id;
            this.Progress.IterationCount = this.Spec.Iterations.Count;


            Task<TestSpecResult> runTask = Task.Factory.StartNew<TestSpecResult>(() =>
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();

                    try
                    {
                        this.CancellationSource.Token.ThrowIfCancellationRequested();

                        RunSpecIterations(specResult);

                        specResult.Status = TestSpecResultStatus.Success;
                    }
                    catch (Exception ex)
                    {
                        // if cancellation was detected then status is cancelled else is error
                        if (ex is OperationCanceledException
                            || (ex is AggregateException && (ex as AggregateException).InnerExceptions.Any(iex => iex is OperationCanceledException)))
                        {
                            // mark as cancelled
                            specResult.Status = TestSpecResultStatus.Cancelled;
                        }
                        else
                        {
                            // mark as error
                            specResult.Status = TestSpecResultStatus.Error;
                            specResult.Exception = ex;
                        }
                    }

                    watch.Stop();
                    specResult.Ms = watch.ElapsedMilliseconds;

                    // insert into spec result repo
                    if (this.SpecResultRepo != null)
                    {
                        this.SpecResultRepo.Insert(specResult);
                    }

                    // return overall result
                    return specResult;

                }, this.CancellationSource.Token);


            this.RunningTasks = new ConcurrentQueue<Task>();
            this.RunningTasks.Enqueue(runTask);

            return runTask;
        }

        private void RunSpecIterations(TestSpecResult specResult)
        {
            if (Spec != null && Spec.Iterations != null && Spec.Iterations.Count > 0)
            {
                // execute each test iteration
                this.Spec.Iterations.ForEach(iteration =>
                    {
                        // report progress
                        Interlocked.Exchange(ref this.Progress.IterationId, iteration.Id);
                        Interlocked.Increment(ref this.Progress.IterationIndex);
                        Interlocked.Exchange(ref this.Progress.IterationRepeatCount, iteration.RepeatCount);
                        Interlocked.Exchange(ref this.Progress.IterationRepeatIndex, 0);

                        RunSpecIteration(iteration, specResult);
                    });
            }
        }

        private void RunSpecIteration(TestSpecIteration<TData> iteration, TestSpecResult specResult)
        {
            this.CancellationSource.Token.ThrowIfCancellationRequested();

            if (iteration.Data == null || iteration.Data.Length == 0)
            {
                throw new ArgumentNullException(string.Format("Test iteration ID:{0} :: data is null or empty.", iteration.Id));
            }


            // execute iteration
            EnsureParallel.RunFor(0, iteration.RepeatCount, iteration.MaxParallelism, this.CancellationSource.Token, index =>
            {
                this.CancellationSource.Token.ThrowIfCancellationRequested();

                // get data
                TData data = iteration.Data[index % iteration.Data.Length];

                // create iteration instance data
                TestSpecIterationInstance<TData> iterationInstance = new TestSpecIterationInstance<TData>(
                    repeatIndex: index,
                    iteration: iteration,
                    data: data,
                    cancellationToken: this.CancellationSource.Token);

                // start watch
                Stopwatch watch = new Stopwatch();
                watch.Start();

                // execute action on data
                object iterationDataRes = this.TestAction(iterationInstance);

                // stop watch
                watch.Stop();

                // get running time
                long ms = watch.ElapsedMilliseconds;

                // create iteration result
                var result = new TestSpecIterationResult();
                result.Id = iteration.Id;
                result.Ms = ms;
                result.ResData = iterationDataRes;
                result.MaxParallelism = iteration.MaxParallelism;
                result.RepeatCount = iteration.RepeatCount;

                // add result to spec results
                specResult.IterationResults.Add(result);

                // report progress
                Interlocked.Increment(ref this.Progress.IterationRepeatIndex);
            });
        }

        /// <summary>
        /// Stop tests.
        /// </summary>
        public void Stop()
        {
            this.CancellationSource.Cancel();
        }

        public void Wait()
        {
            while (RunningTasks.Count > 0)
            {
                this.CancellationSource.Token.ThrowIfCancellationRequested();

                Task runningTask = null;
                if (this.RunningTasks.TryDequeue(out runningTask))
                {
                    runningTask.Wait(this.CancellationSource.Token);
                }
            }
        }

        /// <summary>
        /// Report progress to console.
        /// </summary>
        public void ReportProgressToConsole(Task runTask)
        {
            bool isRunning = true;

            runTask.ContinueWith(t => { isRunning = false; });

            // report progress
            Task progressTask = Task.Factory.StartNew(() =>
            {                
                while (isRunning && this.CancellationSource.IsCancellationRequested == false)
                {
                    this.CancellationSource.Token.WaitHandle.WaitOne(500);

                    Console.Write(string.Format("\r spec={0} - iteration={1} ({2}/{3}) - repeat={4}/{5}", 
                        this.Progress.SpecId, 
                        this.Progress.IterationId, 
                        this.Progress.IterationIndex, 
                        this.Progress.IterationCount,
                        this.Progress.IterationRepeatIndex,
                        this.Progress.IterationRepeatCount).PadRight(Console.WindowWidth, ' '));
                }
            }, this.CancellationSource.Token);


            this.RunningTasks.Enqueue(progressTask);
        }
    }
}
